package com.xuecheng.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.auth.model.po.Company;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.content.model.dto.QueryCourseDetailDto;
import com.xuecheng.content.model.dto.QueryTeachplanMediaDto;
import com.xuecheng.content.model.po.CourseBase;
import com.xuecheng.content.model.po.TeachplanMedia;
import com.xuecheng.media.config.MinioConfig;
import com.xuecheng.media.feignclient.auth.CompanyClient;
import com.xuecheng.media.feignclient.content.CourseBaseInfoClient;
import com.xuecheng.media.feignclient.content.TeachplanMediaClient;
import com.xuecheng.media.mapper.MediaFileMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.model.po.MediaProcess;
import com.xuecheng.media.service.MediaFileService;
import com.xuecheng.media.service.MediaProcessService;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@Service
public class MediaFileServiceImpl extends ServiceImpl<MediaFileMapper, MediaFiles>
    implements MediaFileService {
    @Autowired
    MediaFileService currentProxy;

    @Autowired
    MinioClient minioClient;

    @Autowired
    MinioConfig minioConfig;

    @Autowired
    CompanyClient companyClient;

    @Autowired
    MediaProcessService mediaProcessService;

    @Autowired
    CourseBaseInfoClient courseBaseInfoClient;

    @Autowired
    TeachplanMediaClient teachplanMediaClient;

    /**
     * 上传图片等小文件接口
     * @param file
     * @return
     */
    @Override
    public MediaFiles upload(Long companyId,MultipartFile file,
                             String objectName) {
        //获取文件的类型和名称
        String originalName = file.getOriginalFilename();
        int index = originalName.lastIndexOf(".");
        String suffix = originalName.substring(index);
        String name = originalName.substring(0,index);
        //不要用上面这种直接file.getContentType的方式获取文件类型，通过请求传过来的
        //所以得到的类型都会是multipart/form-data，这样就会有问题，这样浏览器直接访问时
        //由于content-type是multipart/form-data就可能直接下载文件，而不是浏览器里面打开访问
//        String mimeType = file.getContentType();
        //下面这种通过文件扩展名获取的在这里才合适
        String mimeType = getMimeType(suffix);
        String path = StringUtils.isNotBlank(objectName) ? objectName :
                getDefaultFolderPath() + originalName;
        File tempFile = null;
        try {
            //首先创建临时文件，然后拿到临时文件的路径和名称
//            tempFile = File.createTempFile("file_",".temp",
//                    new File("D:\\tempFile"));
            tempFile = File.createTempFile("file_",".temp",
                    new File("E:\\temp"));
            file.transferTo(tempFile);
        }catch(Exception e){
//            XueChengPlusException.cast("上传图片等小文件出错");
            throw new RuntimeException(e);
        }
        uploadFileToMinio(minioConfig.getFileBucket(),path,
                tempFile.getAbsolutePath(),mimeType);
        String md5 = getFileMd5(tempFile);
        Company company = companyClient.getById(companyId);
        MediaFiles mediaFiles = currentProxy.saveMediaFilesToDb(companyId,
                company.getName(), originalName,minioConfig.getFileBucket(),
                path,md5,file.getSize());
        if(mediaFiles == null){
//            XueChengPlusException.cast("文件上传后保存信息失败");
            throw new RuntimeException();
        }
        return mediaFiles;
    }

    /**
     * 获取文件默认目录
     * @return
     */
    private String getDefaultFolderPath(){
        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yy/MM/dd"));
        return date + "/";
    }

    /**
     * 根据扩展名获取mimeType
     * @param extension
     * @return
     */
    public String getMimeType(String extension){
        if(extension == null){
            extension = "";
        }
        ContentInfo contentInfo = ContentInfoUtil.findExtensionMatch(extension);
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if(contentInfo != null){
            mimeType = contentInfo.getMimeType();
        }
        return mimeType;
    }

    /**
     * 将文件上传到minio
     * @param bucket
     * @param object
     * @param fileAbsolutePath
     * @param mimeType
     * @return
     */
    public boolean uploadFileToMinio(String bucket,String object,
            String fileAbsolutePath, String mimeType){
        try{
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket(bucket)
                    .object(object)
                    .filename(fileAbsolutePath)
                    .contentType(mimeType)
                    .build();
            minioClient.uploadObject(uploadObjectArgs);
            log.info("上传文件到minio成功,bucket:{},object:{}", bucket,object);
            return true;
        }catch (Exception e){
            log.error("上传文件出错,bucket:{},object:{},错误信息：{}",
                    bucket,object,e.getMessage());
//            XueChengPlusException.cast("上传文件出错");
            throw new RuntimeException(e);
        }
//        return false;
    }

    /**
     * 获取文件的md5
     * @param file
     * @return
     */
    private String getFileMd5(File file){
        try{
            return DigestUtils.md5Hex(new FileInputStream(file));
        }catch (Exception e){
            throw new RuntimeException();
        }
    }

    @Transactional
    public MediaFiles saveMediaFilesToDb(Long companyId,String companyName,
            String filename,String bucket,String filePath,String md5,Long fileSize){
        if(md5 == null) return null;
        MediaFiles mediaFiles = getOne(new LambdaQueryWrapper<MediaFiles>()
                .eq(MediaFiles::getFileId,md5));
        if(mediaFiles == null){
            mediaFiles = new MediaFiles();
            //这种赋值id的方法有问题，此处改为了雪花算法
            //然后获取mediaProcess的id最大值
//            Long maxId = mediaProcessService.getMaxId();
//            mediaFiles.setId(maxId + 1);
            mediaFiles.setCompanyId(companyId);
            mediaFiles.setCompanyName(companyName);
            mediaFiles.setFilename(filename);
            mediaFiles.setFileType("001002");
            mediaFiles.setBucket(bucket);
            mediaFiles.setFilePath(filePath);
            mediaFiles.setFileId(md5);
            mediaFiles.setUrl("/" + bucket + "/" + filePath);
            mediaFiles.setFileSize(fileSize);
            mediaFiles.setAuditStatus("002003");
            save(mediaFiles);
        }
        Long mediaId = mediaFiles.getId();
        System.out.println("mediaId = " + mediaId);
        //记录视频转码待处理任务
        addWaitingTask(mediaFiles,"transcodeMedia");
        return mediaFiles;
    }

    /**
     * 记录待处理媒资文件相关任务
     * @param mediaFiles
     */
    private void addWaitingTask(MediaFiles mediaFiles,String taskType) {
        String filename = mediaFiles.getFilename();
        String extension = filename.substring(filename.lastIndexOf("."));
        String mimeType = getMimeType(extension);
        //然后判断该文件是否为视频类型
        if(mimeType.equals("video/x-msvideo")){
            MediaProcess mediaProcess = new MediaProcess();
            BeanUtils.copyProperties(mediaFiles,mediaProcess);
            mediaProcess.setTaskType(taskType);
            mediaProcess.setStatus("1");
            mediaProcess.setFailCount(0);
            mediaProcessService.save(mediaProcess);
        }
    }

    /**
     * 媒资文件分页查询
     * @param companyId
     * @param pageParams
     * @param paramsDto
     * @return
     */
    @Override
    public PageResult<MediaFiles> page(Long companyId, PageParams pageParams, QueryMediaParamsDto paramsDto) {
        Page<MediaFiles> pageInfo = new Page<>(pageParams.getPageNo(),pageParams.getPageSize());
        String fileType = paramsDto.getType();
        String filename = paramsDto.getFilename();
        String auditStatus = paramsDto.getAuditStatus();
        page(pageInfo,new LambdaQueryWrapper<MediaFiles>()
                .eq(MediaFiles::getCompanyId,companyId)
                .eq(StringUtils.isNotBlank(fileType),MediaFiles::getFileType,fileType)
                .like(StringUtils.isNotBlank(filename),MediaFiles::getFilename,filename)
                .eq(StringUtils.isNotBlank(auditStatus),MediaFiles::getAuditStatus,auditStatus));
        return new PageResult<>(pageInfo.getRecords(),pageInfo.getCurrent(),
                pageInfo.getSize(),pageInfo.getTotal());
    }

    /**
     * 根据id删除媒资文件
     * @param companyId
     * @param id
     * @return
     */
    @Override
    public RestResponse<String> deleteById(Long companyId, Long id) {
        MediaFiles mediaFiles = getById(id);
        if(mediaFiles == null){
            return RestResponse.validfail("当前媒资文件不存在");
        }
        if(!companyId.equals(mediaFiles.getCompanyId())){
            XueChengPlusException.cast("当前媒资文件不属于本机构，无权操作");
        }
        //然后查询当前媒资文件是否关联了课程计划或者是课程封面等信息
        QueryTeachplanMediaDto tmDto = new QueryTeachplanMediaDto();
        tmDto.setMediaId(id);
        TeachplanMedia teachplanMedia = teachplanMediaClient.getTeachplanMedia(tmDto);
        if(teachplanMedia != null){
            return RestResponse.validfail("该文件关联了课程计划，不能删除");
        }
        QueryCourseDetailDto cdDto = new QueryCourseDetailDto();
        cdDto.setPic(mediaFiles.getUrl());
        CourseBase courseBase = courseBaseInfoClient.getCourseBase(cdDto);
        if(courseBase != null){
            return RestResponse.validfail("该文件是某个课程的封面，暂时无法删除");
        }
        //如果没有，则删除媒资文件
        removeById(id);
        //删除数据库中的记录后，还要删除minio中对应的文件
        deleteFromMinio(mediaFiles.getBucket(),mediaFiles.getFilePath());
        return RestResponse.success("删除成功");
    }

    /**
     * 大文件上传前检查文件是否存在
     * @param fileMd5
     * @return
     */
    @Override
    public RestResponse<Boolean> checkFile(String fileMd5) {
        if(fileMd5 == null){
            XueChengPlusException.cast("文件的md5值为空");
        }
        MediaFiles mediaFiles = getOne(new LambdaQueryWrapper<MediaFiles>()
                .eq(MediaFiles::getFileId, fileMd5));

        String bucket = minioConfig.getVideoBucket();
        String filePath = getComposeFolder(fileMd5,"");
        if(mediaFiles == null){
            //不存在，如果要分片上传文件，就记录待处理任务
            mediaProcessService.recordTask(bucket,filePath,fileMd5,
                    "uploadChunk");
            return RestResponse.success(false);
        }
        //数据库中的信息不为null，然后检查minio中是否存在
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket(mediaFiles.getBucket())
                .object(mediaFiles.getFilePath())
                .build();
        try{
            FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
            if(inputStream != null){
                return RestResponse.success(true);
            }
        }catch (Exception e){
            log.error("路径：{},不存在",mediaFiles.getFilePath());
        }
        //不存在，如果要分片上传文件，就记录待处理任务
        mediaProcessService.recordTask(bucket,filePath,fileMd5,
                "uploadChunk");
        return RestResponse.success(false);
    }

    /**
     * 分块文件上传前检查对应的分块是否存在
     * @param fileMd5
     * @param chunk
     * @return
     */
    @Override
    public RestResponse<Boolean> checkChunk(String fileMd5, Integer chunk) {
        String object = getChunkFileFolderPath(fileMd5) + chunk;
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket(minioConfig.getVideoBucket())
                .object(object)
                .build();
        try{
            FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
            if(inputStream != null) return RestResponse.success(true);
        }catch (Exception e){
            log.error("路径：{},不存在",object);
        }
        return RestResponse.success(false);
    }

    //获取分块文件的目录
    private String getChunkFileFolderPath(String fileMd5){
        return fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/"
                + fileMd5 + "/chunk/";
    }

    /**
     * 上传分块文件
     * @param file
     * @param fileMd5
     * @param chunk
     * @return
     */
    @Override
    public RestResponse<Boolean> uploadChunk(MultipartFile file, String fileMd5, Integer chunk) {
        File tempFile = null;
        try{
            //创建一个临时文件
//            tempFile = File.createTempFile("chunk_",".temp",
//                    new File("D:\\tempFile\\chunkTemp\\"));
            tempFile = File.createTempFile("chunk_",".temp",
                    new File("E:\\temp\\chunkTemp\\"));
            file.transferTo(tempFile);
        }catch(Exception e){
            XueChengPlusException.cast("上传分块文件出错");
        }
        return RestResponse.success(uploadFileToMinio(minioConfig.getVideoBucket(),
                getChunkFileFolderPath(fileMd5) + chunk, tempFile.getAbsolutePath(), file.getContentType()));
    }

    /**
     * 大文件上传后合并分块文件
     * @param fileMd5
     * @param fileName
     * @param chunkTotal
     * @return
     */
    @Override
    public RestResponse<Boolean> mergeChunks(Long companyId,String fileMd5, String fileName, Integer chunkTotal) {
        //合并文件
        String folder = getChunkFileFolderPath(fileMd5);
        //获取文件扩展名
        String fileExt = fileName.substring(fileName.lastIndexOf("."));
        String filePath = getComposeFolder(fileMd5,fileExt);
        //用于获取minio中文件的基本或元数据等信息
        StatObjectResponse stat = null;
        try{
            //先获取要合并的分块文件的源文件
            List<ComposeSource> sources = Stream.iterate(0,i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> ComposeSource.builder()
                            .bucket(minioConfig.getVideoBucket())
                            .object(folder + i)
                            .build())
                    .collect(Collectors.toList());
            //合并分块文件
            ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                    .bucket(minioConfig.getVideoBucket())
                    //上面的源信息的object是要合并的分块文件的路径信息，这个object是合并后的目标文件的路径信息
                    .object(filePath)
                    .sources(sources)
                    .build();
            minioClient.composeObject(composeObjectArgs);
            stat = minioClient.statObject(StatObjectArgs.builder()
                    .bucket(minioConfig.getVideoBucket())
                    .object(filePath)
                    .build());
        }catch(Exception e){
            //合并失败的直接返回错误信息，让用户重新上传，那些合并失败的垃圾分块文件，
            //后续使用xxl-job分布式任务调度定时清理minio中的垃圾分块文件
            XueChengPlusException.cast("合并文件出错");
        }
        //合并好后下载文件
        File downloadFile = downloadFileFromMinio(minioConfig.getVideoBucket(),
                filePath);
        //然后对比md5来校验文件是否完整
        try{
            FileInputStream downloadFileInput = new FileInputStream(downloadFile);
            String downloadMd5 = DigestUtils.md5Hex(downloadFileInput);
            downloadFileInput.close();
            if(!fileMd5.equals(downloadMd5)){
                log.error("文件合并校验失败，object:{}",filePath);
                //删除合并后的文件
//                deleteFromMinio(minioConfig.getVideoBucket(),filePath);
                return RestResponse.validfail(false,
                        "文件合并校验失败，最终上传失败");
            }
        }catch(Exception e){
            log.debug("校验文件失败,fileMd5:{},异常:{}",fileMd5,e.getMessage(),e);
//            deleteFromMinio(minioConfig.getVideoBucket(),filePath);
            return RestResponse.validfail(false,
                    "文件合并校验失败，最终上传失败。");
        }finally{
            if(downloadFile != null){
                downloadFile.delete();
            }
        }
        //文件合并好后要将文件信息保存到数据库
        Company company = companyClient.getById(companyId);
        MediaFiles mediaFiles = currentProxy.saveMediaFilesToDb(companyId, company.getName(),
                fileName,minioConfig.getVideoBucket(),filePath,
                fileMd5, stat.size());
        //文件信息入库后要将上传分块文件的处理任务改为已完成
        mediaProcessService.finishTask(fileMd5,"uploadChunk");
        //文件信息入库后要删除对应的分块文件
        clearChunkFiles(chunkTotal,folder,minioConfig.getVideoBucket());
        return RestResponse.success(mediaFiles != null);
    }

    /**
     * 获取合并后目标文件的目录
     * @param fileMd5
     * @return
     */
    private String getComposeFolder(String fileMd5,String fileExt){
        return fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/"
                + fileMd5 + "/" + fileMd5 + fileExt;
    }

    /**
     * 从minio下载文件到本地
     * @param bucket
     * @param object
     * @return
     */
    public File downloadFileFromMinio(String bucket,String object){
        int lastPointIndex = object.lastIndexOf(".");
        String fileExt = object.substring(lastPointIndex);
        String fileMd5 = object.substring(object.lastIndexOf("/"),
                lastPointIndex);
        String filename = object.substring(object.lastIndexOf("/"));
        //----------------------------------------------------------
        /*
        * 如果用downloadObject下载，那么就可以直接用下面的函数，也不用考虑创建流的问题
        * */
//        DownloadObjectArgs downloadObjectArgs = DownloadObjectArgs.builder()
//                .bucket(minioConfig.getVideoBucket())
//                .object(object)
//                .filename("E:\\minioDownload\\" + filename)
//                .build();
//        minioClient.downloadObject(downloadObjectArgs);
        //----------------------------------------------------------
        File downloadFile = new File("E:\\minioDownload\\" + filename);
//        File downloadFile = new File("D:\\minioDownload\\" + filename);
        try{
            FilterInputStream inputStream = minioClient.getObject(GetObjectArgs
                    .builder()
                    .bucket(bucket)
                    .object(object)
                    .build());
            if(!downloadFile.exists()){
                downloadFile.createNewFile();
            }
            FileOutputStream outputStream = new FileOutputStream(downloadFile);
            IOUtils.copy(inputStream,outputStream);
            inputStream.close();
            outputStream.close();
        }catch (Exception e){
            throw new RuntimeException(e);
//            XueChengPlusException.cast("文件下载出错");
        }
        return downloadFile;
    }

    /**
     * 删除minio中的文件
     * @param bucket
     * @param object
     * @return
     */
    public boolean deleteFromMinio(String bucket,String object){
        try{
            minioClient.removeObject(RemoveObjectArgs.builder()
//                    .bucket(minioConfig.getVideoBucket())
                    .bucket(bucket)
                    .object(object)
                    .build());
        }catch (Exception e){
            log.error("删除minio中的文件失败，object:{}",object);
            throw new RuntimeException(e);
        }
        return true;
    }

    /**
     * 清理分块文件
     * @param chunkTotal
     * @param chunkFolder
     * @param bucket
     */
    private void clearChunkFiles(Integer chunkTotal,String chunkFolder,
                                 String bucket){
        List<DeleteObject> deleteObjectList = Stream.iterate(0,i -> ++i)
                .limit(chunkTotal)
                .map(i -> new DeleteObject(chunkFolder.concat(Integer.toString(i))))
                .collect(Collectors.toList());
        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                .bucket(bucket)
                .objects(deleteObjectList)
                .build();
        /*
        * 注意：如果想要真正删除文件，光调用removeObjects是不够的，
        * 1.因为removeObjects返回的是一个Iterable<Result<DeleteError>>对象，
        * 此方法并不会立即删除文件，而是生成一个待处理的删除任务序列。
        * 2.只有通过遍历迭代器，并调用r.get()方法minio客户端才会真正执行删除，
        * 这是Minio SDK的异步设计特性--删除操作在遍历时被逐个提交。
        * 3.如果未遍历迭代器，删除任务就不会被提交，自然minio中的文件也就不会被删除
        * */
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
        results.forEach(r -> {
            try{
                DeleteError deleteError = r.get();
            }catch(Exception e){
                e.printStackTrace();
            }
        });
    }
}
